package com.yifeng.repo.storage.redis.handler;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.base.Strings;
import com.talkyun.utils.MD5;
import com.yifeng.repo.storage.redis.annotation.Cache;
import com.yifeng.repo.storage.redis.annotation.DelCache;
import com.yifeng.repo.storage.redis.annotation.Lock;
import com.yifeng.repo.base.utils.common.BaseUtil;
import com.yifeng.repo.storage.redis.RedisCacheWorker;
import com.yifeng.repo.storage.redis.RedisLockWorker;
import com.yifeng.repo.storage.redis.configure.RedisAopProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

/**
 * Created by daibing on 2023/2/21.
 */
@Slf4j
public class RedisActionAspectHandler {
    private final String applicationName;
    private final RedisAopProperties properties;
    private final RedisCacheWorker redisCacheWorker;
    private final RedisLockWorker redisLockWorker;
    private final ObjectMapper objectMapper = JsonMapper.builder()
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializationInclusion(JsonInclude.Include.ALWAYS)
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
            .build();

    public RedisActionAspectHandler(String applicationName, RedisAopProperties properties, RedisCacheWorker redisCacheWorker, RedisLockWorker redisLockWorker) {
        this.applicationName = applicationName;
        this.properties = properties;
        this.redisCacheWorker = redisCacheWorker;
        this.redisLockWorker = redisLockWorker;
    }

    public Object cacheDataByAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取基本信息
        Class<?> service = joinPoint.getTarget().getClass();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = this.getMethodByAnnotation(service, methodSignature, Cache.class);
        Cache cacheAnnotation = method.getAnnotation(Cache.class);

        // 2. 准备key
        String keyPrefix = !Strings.isNullOrEmpty(cacheAnnotation.key()) ? cacheAnnotation.key() : service.getName() + "_" + method.getName();
        String keySuffix = this.getKeySuffix(cacheAnnotation.argsIndex(), joinPoint.getArgs());
        String key = properties.getRedisKeyPrefix() + applicationName + ":" + MD5.md5(keyPrefix + "_" + keySuffix);

        // 3. 先从redis获取结果
        String value = redisCacheWorker.get(key);
        if (!Strings.isNullOrEmpty(value)) {
            if (value.equals("null")) {
                return null;
            }
            return objectMapper.readValue(value, TypeFactory.defaultInstance().constructType(method.getGenericReturnType()));
        }

        // 4. redis不存在结果，先抢占锁确保唯一执行
        String lockKey = key + "____concurrent_locker____";
        boolean locked = redisLockWorker.lock(lockKey, BaseUtil.OWNER_ID.toString(), cacheAnnotation.await());

        // 5. 未抢占到锁的持续查询redis
        if (!locked) {
            long endTime = System.currentTimeMillis() + cacheAnnotation.await();
            while (System.currentTimeMillis() <= endTime) {
                Thread.sleep(100);
                value = redisCacheWorker.get(key);
                if (Strings.isNullOrEmpty(value)) {
                    continue;
                }
                if (value.equals("null")) {
                    return null;
                }
                return objectMapper.readValue(value, TypeFactory.defaultInstance().constructType(method.getGenericReturnType()));
            }
        }

        // 6. 抢占到锁的直接操作数据库（未抢占到锁的兜底）
        Object result = joinPoint.proceed();
        if (result != null) {
            String resultText = objectMapper.writeValueAsString(result);
            redisCacheWorker.put(key, cacheAnnotation.timeout(), resultText);
        }
        if (cacheAnnotation.nullable() && result == null) {
            redisCacheWorker.put(key, cacheAnnotation.timeout(), "null");
        }
        return result;
    }

    public void delCacheByAfterReturning(JoinPoint joinPoint) throws Throwable {
        // 1. 获取基本信息
        Class<?> service = joinPoint.getTarget().getClass();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = this.getMethodByAnnotation(service, methodSignature, DelCache.class);
        DelCache delCacheAnnotation = method.getAnnotation(DelCache.class);

        // 2. 准备key
        String keyPrefix = !Strings.isNullOrEmpty(delCacheAnnotation.key()) ? delCacheAnnotation.key() : service.getName() + "_" + method.getName();
        String keySuffix = this.getKeySuffix(delCacheAnnotation.argsIndex(), joinPoint.getArgs());
        String key = properties.getRedisKeyPrefix() + applicationName + ":" + MD5.md5(keyPrefix + "_" + keySuffix);

        // 3. 删除cache
        redisCacheWorker.remove(key);
    }

    public Object lockCacheByAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取基本信息
        Class<?> service = joinPoint.getTarget().getClass();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = this.getMethodByAnnotation(service, methodSignature, Lock.class);
        Lock lockCacheAnnotation = method.getAnnotation(Lock.class);

        // 2. 准备key
        String keyPrefix = !Strings.isNullOrEmpty(lockCacheAnnotation.key()) ? lockCacheAnnotation.key() : service.getName() + "_" + method.getName();
        String keySuffix = this.getKeySuffix(lockCacheAnnotation.argsIndex(), joinPoint.getArgs());
        String key = properties.getRedisKeyPrefix() + applicationName + ":" + MD5.md5(keyPrefix + "_" + keySuffix);

        // 3. 抢占锁
        long endTime = System.currentTimeMillis() + lockCacheAnnotation.await();
        boolean locked = redisLockWorker.lock(key, BaseUtil.OWNER_ID.toString(), lockCacheAnnotation.timeout());
        if (!locked && lockCacheAnnotation.await() <= 0) {
            throw new RuntimeException(String.format("lock failed service=%s, method=%s", service.getName(), method.getName()));
        }
        while (System.currentTimeMillis() <= endTime) {
            Thread.sleep(100);
            locked = redisLockWorker.lock(key, BaseUtil.OWNER_ID.toString(), lockCacheAnnotation.timeout());
            if (locked) {
                break;
            }
        }
        if (!locked) {
            throw new RuntimeException(String.format("lock failed by retry service=%s, method=%s", service.getName(), method.getName()));
        }

        // 4. 执行结果
        try {
            return joinPoint.proceed();
        } finally {
            redisLockWorker.unlock(key, BaseUtil.OWNER_ID.toString());
        }
    }

    private Method getMethodByAnnotation(Class<?> service, MethodSignature methodSignature, Class<? extends Annotation> annotationClass) {
        Method method = methodSignature.getMethod();
        if (method.isAnnotationPresent(annotationClass)) {
            return method;
        }
        try {
            method = service.getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
            if (method.isAnnotationPresent(annotationClass)) {
                return method;
            }
        } catch (NoSuchMethodException e) {
            log.warn("get method failed by exception: [{}:{}], e=", service.getName(), method.getName(), e);
            throw new RuntimeException(String.format("get method failed by exception service=%s, method=%s", service.getName(), method.getName()), e);
        }
        throw new RuntimeException(String.format("get method failed by not found service=%s, method=%s", service.getName(), method.getName()));
    }

    private String getKeySuffix(int[] argsIndex, Object[] args) throws JsonProcessingException {
        if (argsIndex == null || argsIndex.length == 0 || args == null || args.length == 0) {
            return "CONST";
        }
        StringBuilder sb = new StringBuilder();
        for (int index : argsIndex) {
            String argText = objectMapper.writeValueAsString(args[index]);
            sb.append(argText);
        }
        return sb.toString();
    }

}
